package eleme.openapi.sdk.utils;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.lang.reflect.Type;
import java.net.HttpURLConnection;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.net.URLEncoder;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.zip.GZIPInputStream;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import com.fasterxml.jackson.databind.JavaType;

import eleme.openapi.sdk.api.exception.AccessDeniedException;
import eleme.openapi.sdk.api.exception.BusinessException;
import eleme.openapi.sdk.api.exception.ExceedLimitException;
import eleme.openapi.sdk.api.exception.InvalidSignatureException;
import eleme.openapi.sdk.api.exception.InvalidTimestampException;
import eleme.openapi.sdk.api.exception.MethodNotAllowedException;
import eleme.openapi.sdk.api.exception.PermissionDeniedException;
import eleme.openapi.sdk.api.exception.ServerErrorException;
import eleme.openapi.sdk.api.exception.ServiceException;
import eleme.openapi.sdk.api.exception.SourceTimeoutException;
import eleme.openapi.sdk.api.exception.UnauthorizedException;
import eleme.openapi.sdk.api.exception.ValidationFailedException;
import eleme.openapi.sdk.api.protocol.ErrorPayload;
import eleme.openapi.sdk.api.protocol.ResponsePayload;
import eleme.openapi.sdk.config.Config;
import eleme.openapi.sdk.config.Constants;
import eleme.openapi.sdk.oauth.response.Token;

/**
 * @author me
 */
public abstract class WebUtils {

    private static final String DEFAULT_CHARSET = Constants.CHARSET_UTF8;
    private static final String METHOD_POST = "POST";
    private static final String HTTPS = "https";
    private static final String GZIP = "gzip";

    private static class DefaultTrustManager implements X509TrustManager {

        @Override
        public void checkClientTrusted(X509Certificate[] chain, String authType) {
        }

        @Override
        public void checkServerTrusted(X509Certificate[] chain, String authType) {
        }

        @Override
        public X509Certificate[] getAcceptedIssuers() {
            return null;
        }

    }

    /**
     * 发送 HTTP/HTTPS POST 请求
     */
    public static String doPost(Config context, String url,
            Map<String, String> params,
            String charset,
            int connectTimeout,
            int readTimeout,
            Map<String, String> headerMap,
            String requestId) throws IOException {

        String contentType = "application/x-www-form-urlencoded; charset=" + charset;
        String query = buildQuery(params, charset);

        byte[] content = {};
        if (query != null) {
            content = query.getBytes(charset);
        }
        return sendPost(context, url, contentType, content, connectTimeout, readTimeout, headerMap, requestId);
    }

    public static String buildQuery(Map<String, String> params, String charset) throws IOException {
        if (params == null || params.isEmpty()) {
            return null;
        }

        StringBuilder query = new StringBuilder(50);
        Set<Map.Entry<String, String>> entrySet = params.entrySet();

        boolean hasParam = false;
        for (Map.Entry<String, String> param : entrySet) {
            String name = param.getKey();
            String value = param.getValue();
            // 忽略参数名或参数值为空的参数
            if (StringUtils.areNotEmpty(name, value)) {
                if (hasParam) {
                    query.append("&");
                } else {
                    hasParam = true;
                }
                query.append(name).append("=").append(URLEncoder.encode(value, charset));
            }
        }
        return query.toString();
    }

    /**
     * 发送 HTTP/HTTPS POST 请求
     */
    public static String doPost(Config config, String url,
            String contentType,
            byte[] content,
            int connectTimeout,
            int readTimeout,
            Map<String, String> headerMap,
            String requestId)
            throws IOException {
        return sendPost(config, url, contentType, content, connectTimeout, readTimeout, headerMap, requestId);
    }

    private static String sendPost(Config config, String url, String contentType, byte[] content, int connectTimeout, int readTimeout,
            Map<String, String> headerMap, String requestId) throws IOException {
        HttpURLConnection conn = null;
        OutputStream out = null;
        String rsp;
        try {
            conn = getConnection(new URL(url), connectTimeout, readTimeout, METHOD_POST, contentType, headerMap, requestId);

            // 建立连接
            conn.connect();

            out = conn.getOutputStream();

            // 发送请求参数
            out.write(content);
            out.flush();

            // 获取响应
            rsp = getResponseAsString(conn);
        } finally {
            if (out != null) {
                out.close();
            }
            if (conn != null) {
                conn.disconnect();
            }
        }
        return rsp;
    }

    private static HttpURLConnection getConnection(URL url, int connectTimeout, int readTimeout,
            String method, String contentType, Map<String, String> headerMap, String requestId)
            throws IOException {
        HttpURLConnection conn;
        if (HTTPS.equals(url.getProtocol())) {
            SSLContext ctx;
            try {
                ctx = SSLContext.getInstance("TLS");
                ctx.init(new KeyManager[0], new TrustManager[]{new DefaultTrustManager()}, new SecureRandom());
            } catch (Exception e) {
                throw new IOException(e);
            }
            HttpsURLConnection httpsConn = (HttpsURLConnection) url.openConnection();
            httpsConn.setSSLSocketFactory(ctx.getSocketFactory());
            httpsConn.setHostnameVerifier(new HostnameVerifier() {
                @Override
                public boolean verify(String hostname, SSLSession session) {
                    // 默认都认证通过
                    return true;
                }
            });
            conn = httpsConn;
        } else {
            conn = (HttpURLConnection) url.openConnection();
        }

        // 提交模式
        conn.setRequestMethod(method);

        // 发送POST请求必须设置如下两行
        conn.setDoInput(true);
        conn.setDoOutput(true);

        // Post 请求不能使用缓存
        conn.setUseCaches(false);

        // 设置连接主机超时（单位：毫秒）
        conn.setConnectTimeout(connectTimeout);
        // 设置从主机读取数据超时（单位：毫秒）
        conn.setReadTimeout(readTimeout);

        conn.setRequestProperty("Accept", "application/json; charset=UTF-8");
        conn.setRequestProperty("Content-Type", contentType);
        conn.setRequestProperty("Accept-Encoding", "gzip");
        conn.setRequestProperty("User-Agent", "eleme-openapi-java-sdk");
        conn.setRequestProperty("x-openapi-requestid", requestId);

        if (headerMap != null) {
            for (Map.Entry<String, String> entry : headerMap.entrySet()) {
                conn.setRequestProperty(entry.getKey(), entry.getValue());
            }
        }
        return conn;
    }

    protected static String getResponseAsString(HttpURLConnection conn) throws IOException {
        String charset = getResponseCharset(conn.getContentType());
        InputStream error = conn.getErrorStream();
        if (error != null) {
            String msg = getStreamAsString(conn, error, charset);
            if (StringUtils.isEmpty(msg)) {
                throw new IOException(conn.getResponseCode() + ':' + conn.getResponseMessage());
            } else {
                return msg;
            }
        } else {
            return getStreamAsString(conn, conn.getInputStream(), charset);
        }
    }

    private static String getStreamAsString(HttpURLConnection conn, InputStream in, String charset) throws IOException {
        try {
            Reader reader = GZIP.equals(conn.getContentEncoding()) ?
                    new InputStreamReader(new GZIPInputStream(in), charset) :
                    new InputStreamReader(in, charset);

            StringBuilder response = new StringBuilder();
            final char[] buff = new char[1024];
            int read;
            while ((read = reader.read(buff)) > 0) {
                response.append(buff, 0, read);
            }
            return response.toString();
        } finally {
            if (in != null) {
                in.close();
            }
        }
    }

    private static String getResponseCharset(String contentType) {
        String charset = DEFAULT_CHARSET;
        if (!StringUtils.isEmpty(contentType)) {
            String[] params = contentType.split(";");
            for (String param : params) {
                param = param.trim();
                if (param.startsWith("charset")) {
                    String[] pair = param.split("=", 2);
                    if (pair.length == 2) {
                        if (!StringUtils.isEmpty(pair[1])) {
                            charset = pair[1].trim();
                        }
                    }
                    break;
                }
            }
        }
        return charset;
    }

    /**
     * 调用 OpenAPI 服务
     *
     * @param context 配置信息
     * @param action 服务标识
     * @param parameters 服务参数
     * @param token Token
     * @param type 返回值类型
     * @param <T> 返回值泛型类型
     * @return 返回数据
     * @throws ServiceException 调用服务异常
     */
    public static <T> T call(Config context, String action, Map<String, Object> parameters, Token token, Type type)
            throws ServiceException {

        if (type == void.class) {
            return null;
        }

        final long timestamp = System.currentTimeMillis();
        final String appKey = context.getAppKey();
        final String secret = context.getAppSecret();
        final String accessToken = token.getAccessToken();
        final String requestId = getReqID();

        System.out.println("requestId: " + requestId);
        Map<String, Object> requestPayload = new HashMap<String, Object>(7);
        requestPayload.put("nop", "1.0.0");
        requestPayload.put("id", requestId);
        requestPayload.put("action", action);
        requestPayload.put("token", accessToken);

        Map<String, Object> metasHashMap = new HashMap<String, Object>(2);
        metasHashMap.put("app_key", appKey);
        metasHashMap.put("timestamp", timestamp);

        requestPayload.put("metas", metasHashMap);
        requestPayload.put("params", parameters);
        String signature = SignatureUtil.generateSignature(appKey, secret, timestamp, action, accessToken, parameters);
        requestPayload.put("signature", signature);

        String requestJson = JacksonUtils.obj2json(requestPayload);
        ResponsePayload responsePayload;
        try {
            responsePayload = doRequest(context, requestJson, requestId);
        } catch (SocketTimeoutException ex) {
            throw new SourceTimeoutException();
        } catch (IOException ex) {
            throw new ServiceException(ex.getClass().getName(), ex);
        }
        setLogInfo(context, "request: " + requestJson);

        if (responsePayload == null) {
            throw new ServiceException("调用服务返回数据为空：" + action);
        }
        if (responsePayload.getError() != null) {
            ServiceException ex = toException(responsePayload.getError());
            if (ex != null) {
                setLogError(context, "error: " + ex.getMessage());
                throw ex;
            }
            throw new ServerErrorException(action);
        }

        String json = JacksonUtils.obj2json(responsePayload.getResult());
        JavaType javaType = JacksonUtils.getInstance().getTypeFactory().constructType(type);
        return JacksonUtils.json2obj(json, javaType);
    }

    public static String getReqID() {
        return generateUUID(32) + '|' + System.currentTimeMillis();
    }

    private static String generateUUID(int length) {
        try {
            String uuid = UUID.randomUUID().toString().replaceAll("-", "");
            if (uuid.length() > length) {
                uuid = uuid.substring(0, length);
            }
            return uuid.toUpperCase();
        } catch (Exception e) {
            return "00112233445566778899AABBCCDDEEFF";
        }
    }

    private static ResponsePayload doRequest(Config context, String requestJson, String requestId)
            throws IOException {
        String response = sendPost(context, context.getApiUrl(), "application/json; charset=utf-8",
                requestJson.getBytes(Constants.CHARSET_UTF8), 15000, 15000, null, requestId);
        setLogInfo(context, "response: " + response);
        return JacksonUtils.json2obj(response, ResponsePayload.class);
    }

    private static ServiceException toException(ErrorPayload error) throws ServiceException {
        String code = error.getCode();
        if (StringUtils.isEmpty(code)) {
            return null;
        }
        String message = error.getMessage();
        if ("ACCESS_DENIED".equals(code)) {
            return new AccessDeniedException(message);
        }
        if ("EXCEED_LIMIT".equals(code)) {
            return new ExceedLimitException(message);
        }
        if ("INVALID_SIGNATURE".equals(code)) {
            return new InvalidSignatureException(message);
        }
        if ("INVALID_TIMESTAMP".equals(code)) {
            return new InvalidTimestampException(message);
        }
        if ("METHOD_NOT_ALLOWED".equals(code)) {
            return new MethodNotAllowedException(message);
        }
        if ("PERMISSION_DENIED".equals(code)) {
            return new PermissionDeniedException(message);
        }
        if ("UNAUTHORIZED".equals(code)) {
            return new UnauthorizedException(message);
        }
        if ("VALIDATION_FAILED".equals(code)) {
            return new ValidationFailedException(message);
        }
        if ("BUSINESS_ERROR".equals(code) || code.startsWith("BIZ_")) {
            return new BusinessException(code, error.getMessage());
        }
        return new ServiceException(message);
    }

    private static void setLogInfo(Config context, String msg) {
        if (null != context.getSdkLogger()) {
            context.getSdkLogger().info(msg);
        }
    }

    private static void setLogError(Config context, String msg) {
        if (null != context.getSdkLogger()) {
            context.getSdkLogger().error(msg);
        }
    }

}
